/*
 *  ********************************************************************   **
 *  Copyright notice                                                       **
 *  **																	   **
 *  (c) 2003 Entagged Developpement Team				                   **
 *  http://www.sourceforge.net/projects/entagged                           **
 *  **																	   **
 *  All rights reserved                                                    **
 *  **																	   **
 *  This script is part of the Entagged project. The Entagged 			   **
 *  project is free software; you can redistribute it and/or modify        **
 *  it under the terms of the GNU General Public License as published by   **
 *  the Free Software Foundation; either version 2 of the License, or      **
 *  (at your option) any later version.                                    **
 *  **																	   **
 *  The GNU General Public License can be found at                         **
 *  http://www.gnu.org/copyleft/gpl.html.                                  **
 *  **																	   **
 *  This copyright notice MUST APPEAR in all copies of the file!           **
 *  ********************************************************************
 */

package entagged.tageditor.tools.renaming;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.regex.Pattern;

import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

import entagged.listing.ListingProcessor;
import entagged.listing.gui.ListingProgressDialog;
import entagged.tageditor.TagEditorFrame;
import entagged.tageditor.resources.LangageManager;
import entagged.tageditor.tools.renaming.data.DirectoryDescriptor;
import entagged.tageditor.tools.renaming.data.DirectoryPruneStructure;
import entagged.tageditor.tools.renaming.data.FileDescriptor;
import entagged.tageditor.tools.renaming.data.ProcessingResult;
import entagged.tageditor.tools.renaming.data.RenameConfiguration;
import entagged.tageditor.tools.renaming.data.stat.properties.CanNotWriteProperty;
import entagged.tageditor.tools.renaming.data.stat.properties.ContainsWarningsProperty;
import entagged.tageditor.tools.renaming.data.stat.properties.DestinationExistsProperty;
import entagged.tageditor.tools.renaming.data.stat.properties.DuplicateErrorProperty;
import entagged.tageditor.tools.renaming.data.stat.properties.NoChangeProperty;
import entagged.tageditor.tools.renaming.gui.InspectionControl;
import entagged.tageditor.tools.renaming.model.DefaultTreeModel;
import entagged.tageditor.tools.renaming.model.TargetTreeFilter;
import entagged.tageditor.tools.renaming.pattern.DirectoryPattern;
import entagged.tageditor.tools.renaming.pattern.FilePattern;
import entagged.tageditor.tools.renaming.processing.FileProcessor;
import entagged.tageditor.tools.stringtransform.TransformSet;

/**
 * This class is meant for performing file movement or file renaming.<br>
 * The {@link #run()} method will start the scanning of the given selection,
 * present the results, and rename and moves the audiofiles.<br>
 * <b>Consider:</b> This class will show dialogs, its not just processing.
 * 
 * @author Christian Laireiter
 */
public final class FileRenamer implements Runnable {

	/**
	 * This constant represents the placeholder for the album of an audio tag.<br>
	 * It is used to define patterns.
	 */
	public static final String ALBUM_MASK = "<album>";

	/**
	 * This constant represents the placeholder for the artist of an audio tag.<br>
	 * It is used to define patterns.
	 */

	public static final String ARTIST_MASK = "<artist>";

	/**
	 * This constant represents the placeholder for the bitrate of an audio tag.<br>
	 * It is used to define patterns.
	 */
	public static final String BITRATE_MASK = "<bitrate>";

	/**
	 * This constant represents the placeholder for the comment of an audio tag.<br>
	 * It is used to define patterns.
	 */
	public static final String COMMENT_MASK = "<comment>";

	/**
	 * This constant represents the placeholder for the genre of an audio tag.<br>
	 * It is used to define patterns.
	 */
	public static final String GENRE_MASK = "<genre>";

	/**
	 * This field contains a {@link Pattern}object which is compiled to
	 * identify a set of invalid characters for directory specification. <br>
	 * One can easily replace all defined characters of a string by doing the
	 * following: <br>
	 * 
	 * <pre>
	 * String path = &quot;C:\\temp\\Hello%World&quot;;
	 * Matcher matcher = PATTERN_INVALID_DIRECTORY_CHARS.matcher(path);
	 * path = matcher.replaceAll(&quot;-&quot;);
	 * </pre>
	 * 
	 * <br>
	 * No Path will be "C:\temp\Hello-World"; <br>
	 * <br>
	 * Using this will speed up multiple replacements.
	 */
	public final static Pattern PATTERN_INVALID_DIRECTORY_CHARS = Pattern
			.compile("(\\:|\\?|\\*|\\\"|\\'|\\<|\\>|\\||\\!|\\%|\\Q"
					+ File.separatorChar + "\\E|\\Q" + File.pathSeparatorChar
					+ "\\E)");

	/**
	 * Like {@link #PATTERN_INVALID_DIRECTORY_CHARS}this field defines the same
	 * chars plus path separators ("\" and "/") and
	 * System.getProperty("path.separator") <br>
	 */
	public final static Pattern PATTERN_INVALID_FILE_CHARS = Pattern
			.compile("(\\:|\\?|\\*|\\\"|\\'|\\<|\\>|\\||\\!|\\%|\\\\|\\/|\\Q"
					+ File.separatorChar + "\\E|\\Q" + File.pathSeparatorChar
					+ "\\E)");

	/**
	 * This constant represents the placeholder for the title of an audio tag.<br>
	 * It is used to define patterns.
	 */
	public static final String TITLE_MASK = "<title>";

	/**
	 * This constant represents the placeholder for the track of an audio tag.<br>
	 * It is used to define patterns.
	 */
	public static final String TRACK_MASK = "<track>";

	/**
	 * This constant represents the placeholder for the year of an audio tag.<br>
	 * It is used to define patterns.
	 */
	public static final String YEAR_MASK = "<year>";

	/**
	 * If this is set to <code>true</code>, the user aborted the operation.
	 */
	private boolean aborted = false;

	/**
	 * This field recieves a parent for the preview window.
	 */
	private JFrame parentFrame = null;

	/**
	 * Stores the result of the processing.<br>
	 */
	private ProcessingResult processingResult;

	/**
	 * This field will be assigned at each call of {@link #run()}. <br>
	 * The methode {@link #renameFile(FileDescriptor)} will fill it with those
	 * directories, which are the source of a handeled file and not a target of
	 * any other (this is easy due to a Flag).
	 */
	private DirectoryPruneStructure pruneStruct;

	/**
	 * This field stores the configuration of the current operation.<br>
	 */
	private RenameConfiguration renameConfig;

	/**
	 * Creates an instance of the file renamin operation.<br>
	 * 
	 * @param selection
	 *            The files or directories which should be processed.
	 * @param directoryPattern
	 *            The directorypattern for the operation. May be
	 *            <code>null</code> to indicate that no movement should be
	 *            performed.<br>
	 * @param filePattern
	 *            The filenamepattern for the operation. May be
	 *            <code>null</code> to indicate that the filename should be
	 *            preserved.<br>
	 * @param transformSet
	 *            The set of transformation which will be used.
	 */
	public FileRenamer(File[] selection, String directoryPattern,
			String filePattern, TransformSet transformSet) {
		assert selection != null;
		this.renameConfig = new RenameConfiguration(selection,
				new DirectoryPattern(directoryPattern), new FilePattern(
						filePattern), transformSet);
		/*
		 * Applay transfomations
		 */
		this.renameConfig.getDirectoryPattern().setTransformSet(transformSet);
		this.renameConfig.getFilePattern().setTransformSet(transformSet);
		this.processingResult = new ProcessingResult();
	}

	/**
	 * Creates an instance of the file renamin operation.<br>
	 * 
	 * @param previewParent
	 *            A frame, which becomes parent of the prieview winodw.
	 * @param selection
	 *            The files or directories which should be processed.
	 * @param directoryPattern
	 *            The directorypattern for the operation. May be
	 *            <code>null</code> to indicate that no movement should be
	 *            performed.<br>
	 * @param filePattern
	 *            The filenamepattern for the operation. May be
	 *            <code>null</code> to indicate that the filename should be
	 *            preserved.<br>
	 * @param transformSet
	 *            The set of transformation which will be used.
	 */
	public FileRenamer(JFrame previewParent, File[] selection,
			String directoryPattern, String filePattern,
			TransformSet transformSet) {
		this(selection, directoryPattern, filePattern, transformSet);
		this.parentFrame = previewParent;
	}

	/**
	 * This method will copy the <code>file</code> to the given
	 * <code>targetFile</code>
	 * 
	 * @param file
	 *            The file to be copied.
	 * @param targetFile
	 *            The destination.
	 */
	private void copyFile(File file, File targetFile) {
		try {
			BufferedInputStream bis = new BufferedInputStream(
					new FileInputStream(file));
			BufferedOutputStream bos = new BufferedOutputStream(
					new FileOutputStream(targetFile));
			byte[] tmp = new byte[8192];
			int read = -1;
			while ((read = bis.read(tmp)) > 0) {
				bos.write(tmp, 0, read);
			}
			bis.close();
			bos.close();
		} catch (Exception e) {
			if (!targetFile.delete()) {
				System.err.println("Ups, created copy cant be deleted ("
						+ targetFile.getAbsolutePath() + ")");
			}
		}
	}

	/**
	 * This method returns the configuration object.
	 * 
	 * @return Configuration of renaming.
	 */
	public RenameConfiguration getConfig() {
		return this.renameConfig;
	}

	/**
	 * @return Returns the parentFrame.
	 */
	public JFrame getParentFrame() {
		return this.parentFrame;
	}

	/**
	 * Returns the result structure.
	 * 
	 * @return Result structure.
	 */
	public ProcessingResult getResult() {
		return this.processingResult;
	}

	/**
	 * This method reads the children of the current parent out of the treemodel
	 * and renames them (if they represent files).
	 * 
	 * @param lpd
	 *            The progress dialog.
	 * @param treemodel
	 *            The treemodel.
	 * @param currentParent
	 *            The current parent object (will be a directory).
	 */
	private void handleChildren(ListingProgressDialog lpd,
			DefaultTreeModel treemodel, Object currentParent) {
		int childCount = treemodel.getChildCount(currentParent);
		lpd.directoryChanged(childCount, currentParent.toString());
		for (int i = 0; i < childCount && !this.aborted; i++) {
			this.aborted = lpd.abort();
			Object currentChild = treemodel.getChild(currentParent, i);
			if (!treemodel.isLeaf(currentChild)) {
				handleChildren(lpd, treemodel, currentChild);
				lpd.directoryChanged(childCount, currentParent.toString());
			} else {
				lpd.fileChanged(i, currentChild.toString());
				// now we have got a file, its getting interesting.
				if (currentChild instanceof FileDescriptor) {
					renameFile((FileDescriptor) currentChild);
				}
			}
		}
	}

	/**
	 * Returns <code>true</code> if the user aborted the operation.
	 * 
	 * @return <code>true</code> if renaming was aborted.
	 */
	public boolean isAborted() {
		return this.aborted;
	}

	/**
	 * Will prune the statistics of the given descriptors.<br>
	 * 
	 * @param fileSystemRoots
	 *            directories to process.
	 */
	private void refreshStatistics(DirectoryDescriptor[] fileSystemRoots) {
		for (int i = 0; i < fileSystemRoots.length; i++) {
			fileSystemRoots[i]
					.refreshStat(DestinationExistsProperty.PROPERTY_NAME);
			fileSystemRoots[i]
					.refreshStat(DuplicateErrorProperty.PROPERTY_NAME);
			fileSystemRoots[i]
					.refreshStat(ContainsWarningsProperty.PROPERTY_NAME);
		}
	}

	/**
	 * Renames the given file.<br>
	 * <b>Consider:</b> {@link #pruneStruct} must not be <code>null</code>.
	 * 
	 * @param descriptor
	 *            The file descriptor, which represents the file to rename.
	 */
	private void renameFile(FileDescriptor descriptor) {
		if (descriptor.getStatistic().getProperty(
				NoChangeProperty.PROPERTY_NAME) > 0) {
			// The file doesn't need to be renamed.
			return;
		}
		DirectoryDescriptor targetDir = descriptor.getTargetDirectory();
		if (targetDir != null) {
			File targetDirFile = new File(targetDir.getPath());
			if (!targetDirFile.exists()) {
				if (!targetDirFile.mkdirs()) {
					throw new RuntimeException("Can't create "
							+ targetDir.getPath());
				}
			}
			/*
			 * Now check one more time if the target exists.
			 */
			File targetFile = new File(descriptor.getTargetDirectory()
					.getPath()
					+ File.separator + descriptor.getTargetName());
			if (targetFile.exists()) {
				throw new RuntimeException("Target file already exists \n"
						+ targetFile.getAbsolutePath());
			}
			/*
			 * Add the source directory for pruning. One could think to not add
			 * the current directory, if it is marked as a target. But it may
			 * happen, that a directory is marked as target, since a file would
			 * be placed there if the file could be moved. If the file cannot be
			 * moved the directory descriptor still exists or is marked as
			 * target, since one could use the option to copy those files. So
			 * add it and exclude it
			 */
			pruneStruct.addDirectory(descriptor.getSourceDirectory().getPath());
			if (descriptor.getStatistic().getProperty(
					CanNotWriteProperty.PROPERTY_NAME) > 0) {
				// Here we must copy, but only if it was requested by user.
				// Request ist normally not necessary, however if the filter
				// malfunctions...
				if (renameConfig.isCopyUnmodifiableFiles()) {
					copyFile(new File(descriptor.getPath()), targetFile);
					// Tell the pruning, that the source directory must not
					// be considered for deletion, since this file resides.
					pruneStruct.excludeDirectory(descriptor.getPath());
				}
			} else {
				File sourceFile = new File(descriptor.getPath());
				if (!sourceFile.renameTo(targetFile)) {
					// For time of programming an exception is thrown. It
					// may be possible that another one than me would remove
					// the throw statement. In this case let the pruning
					// facility
					// know, that the source directory won't be empty
					pruneStruct.excludeDirectory(descriptor
							.getSourceDirectory().getPath());
					throw new RuntimeException("can't rename (\n"
							+ sourceFile.getAbsolutePath() + "\nto\n"
							+ targetFile.getAbsolutePath() + ")");
				}
			}
		}
	}

	/**
	 * This method traverses the tree structure using the
	 * {@link entagged.tageditor.tools.renaming.model.DefaultTreeModel} in
	 * conjunction with
	 * {@link entagged.tageditor.tools.renaming.model.TargetTreeFilter}.<br>
	 * This is what the user has seen and confirmed.
	 */
	private void renameFiles() {
		DefaultTreeModel treemodel = new DefaultTreeModel(processingResult
				.getFileSystemRoots(), new TargetTreeFilter(getConfig()),
				"arbitrary");
		Object currentParent = treemodel.getRoot();
		/*
		 * Create gui
		 */
		final ListingProgressDialog lpd = new ListingProgressDialog(parentFrame);
		lpd.setModal(true);
		lpd.setLocationRelativeTo(null);
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				// Blocks entagged parent frame if given.
				lpd.setVisible(true);
			}

		});
		try {
			handleChildren(lpd, treemodel, currentParent);
			lpd.appendMessage(LangageManager.getProperty("tagrename.prune"));
			pruneStruct.prune();
		} catch (RuntimeException re) {
			lpd.dispose();
			throw re;
		} finally {
			lpd.processingFinished();
			lpd.dispose();
		}
	}

	/**
	 * (overridden) This method performs the processing, the result display and
	 * the renaming.
	 * 
	 * @see java.lang.Runnable#run()
	 */
	public void run() {
		this.pruneStruct = new DirectoryPruneStructure();
		// Tell the pruning, that the parents of the selection are not to be
		// deleted.
		File[] selection = renameConfig.getSelection();
		for (int i = 0; i < selection.length; i++) {
			if (selection[i].getParent() != null)
				pruneStruct.excludeDirectory(selection[i].getParent());
		}
		try {
			ListingProcessor lpc = new ListingProcessor(new FileProcessor(
					this.processingResult, renameConfig), renameConfig
					.getSelection());
			ListingProgressDialog lpd = new ListingProgressDialog(
					getParentFrame());
			lpd.setLocationRelativeTo(null);
			lpc.setListingProgressListener(lpd);
			lpd.setModal(false);
			lpd.setVisible(true);
			lpc.start().join();
			lpd.processingFinished();
			lpd.dispose();
			// If an exception has occured during processing
			if (lpc.getLastSeriouseException() != null) {
				// Throw it, this blocks exception catch diplays the error.
				throw lpc.getLastSeriouseException();
			}
			// If not aborted, display results
			if (!lpd.abort()) {
				/*
				 * Now some statistics must be refreshed if the directory pattern
				 * contains a bitrate.
				 * If the bitrate pattern is replaced by its value it may
				 * be possible a conflict arises.
				 */
				refreshStatistics(getResult().getFileSystemRoots());
				InspectionControl control = new InspectionControl(this);
				control.displayInspector();
				if (!(aborted = control.isAborted())) {
					// Now the renaming can begin, using the targettreemodel
					renameFiles();
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
			JOptionPane.showMessageDialog(getParentFrame(), LangageManager
					.getProperty("tagrename.errormsg")
					+ "\n" + e.getMessage(), LangageManager
					.getProperty("common.dialog.error"),
					JOptionPane.ERROR_MESSAGE);
		} finally {
			if (this.parentFrame != null
					&& this.parentFrame instanceof TagEditorFrame) {
				((TagEditorFrame) parentFrame).refreshCurrentTableView();
			}
		}
	}

	/**
	 * This method enables or disables the copy of unmoveable files.
	 * 
	 * @param b
	 *            <code>true</code>, if unmoveable files should be copied
	 */
	public void setCopyUnmodifiableFiles(boolean b) {
		if (getConfig().isCopyUnmodifiableFiles() != b) {
			getConfig().setCopyUnmodifiableFiles(b);
			// Update the bitrate patterns
			if (getConfig().getDirectoryPattern().containsBitrate()) {
				FileProcessor.processBitrate(getResult().getFileSystemRoots(),
						getConfig().isCopyUnmodifiableFiles());
				refreshStatistics(getResult().getFileSystemRoots());
			}
		}
	}
}
